package com.puzzlebazar.client.hashiwokakero;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Vector;

import com.google.gwt.user.client.ui.HasText;
import com.puzzlebazar.client.puzzle.model.Move;
import com.puzzlebazar.client.puzzle.model.MoveClearPuzzle;
import com.puzzlebazar.client.puzzle.model.MoveImpl;
import com.puzzlebazar.client.puzzle.model.MoveList;
import com.puzzlebazar.client.puzzle.model.PuzzleFactory;
import com.puzzlebazar.client.squaregrid.model.SGModelImpl;
import com.puzzlebazar.client.squaregrid.view.ComponentHighlighter;
import com.puzzlebazar.client.util.BoundInfo;
import com.puzzlebazar.client.util.Vec2i;

public final class HashiwokakeroModel extends SGModelImpl {
	
	/**
	 * 
	 */
	private static final long serialVersionUID = -4588399751156466065L;

	private HashMap<Vec2i, Island> islands = new HashMap<Vec2i, Island>();
	private Vector<Bridge> bridges = new Vector<Bridge>();
	private Boolean        usageMap[][];
	
	/**
	 * Adds an island at the specified location with the specified value.
	 * Will remove any island using the current location. Notifies observers 
	 * after the change. Returns a list of all the unit moves required to add 
	 * the island, including any island or bridge destruction. 
	 * The user should make sure to call notifyObservers( moveList ) after this move,
	 * passing along the generated move list,
	 * or notifyObservers() to refresh the entire board.
	 * 
	 * @param loc The location at which to add the island
	 * @param value The value of the island to add
	 * @return The list of all unit moves required to add the island
	 * @throws Exception
	 */
	public MoveList addIsland( Vec2i loc, int value ) throws Exception {
		if( !isValidCell(loc) )
			throw new Exception( "Invalid cell location" );

		MoveList moveList = new MoveList( MoveImpl.NO_TIME );
		addIsland( loc, value, moveList );
		setChanged();
		
		return moveList;
	}

	/**
	 * Removes an island from a specified location. Will remove any bridge connected
	 * to this island. Notifies observers after the change. Returns a list of all the
	 * unit moves required to remove the island, including any bridge destruction.
	 * The user should make sure to call notifyObservers( moveList ) after this move,
	 * passing along the generated move list,
	 * or notifyObservers() to refresh the entire board.
	 * 
	 * @param loc The location at which to remove an island
	 * @return The list of all unit moves required to remove the island
	 * @throws Exception
	 */
	public MoveList removeIsland( Vec2i loc ) throws Exception {
		if( !isValidCell(loc) )
			throw new Exception( "Invalid cell location" );

		MoveList moveList = new MoveList( MoveImpl.NO_TIME );
		removeIsland( loc, moveList );		
		setChanged();		
		
		return moveList;
	}

	/**
	 * Adds a bridge between the specified locations. Will remove any bridge over the 
	 * exact same locations. Fails if crossing any other bridge or island.
	 * Notifies observers after the change. Returns a list of all the unit moves 
	 * required to add the bridge, including any bridge destruction.
	 * The user should make sure to call notifyObservers( moveList ) after this move,
	 * passing along the generated move list,
 	 * or notifyObservers() to refresh the entire board.
	 * 
	 * @param from The starting location for the bridge
	 * @param to The ending location for the bridge
	 * @param value The value of the bridge
	 * @return The list of all unit moves required to add the bridge
	 * @throws Exception
	 */
	public MoveList addBridge( Vec2i from, Vec2i to,  int value ) throws Exception {

		MoveList moveList = new MoveList( MoveImpl.NO_TIME );
		addBridge( from, to, value, moveList );		
		setChanged();		
		
		return moveList;
	}

	/**
	 * Removes a bridge between the specified locations. Return the list of all unit
	 * moves required to remove the bridge.
	 * The user should make sure to call notifyObservers( moveList ) after this move,
	 * passing along the generated move list,
 	 * or notifyObservers() to refresh the entire board.
	 * 
	 * @param from The starting location for the bridge
	 * @param to The ending location for the bridge
	 * @return The list of all unit moves required to add the bridge
	 * @throws Exception
	 */
	public MoveList removeBridge( Vec2i from,  Vec2i to ) throws Exception {
		
		MoveList moveList = new MoveList( MoveImpl.NO_TIME );
		removeBridge( from, to, moveList );		
		setChanged();
		
		return moveList;
	}	
	
	/**
	 * Adds an island at the specified location and appends to the move list 
	 * while doing so. Will remove any island already sitting there, and any 
	 * bridge connected to the removed island.
	 * 
	 * @param loc The location at which to add an island
	 * @param value The value of the island to add
	 * @param moveList The list of moves to populate
	 * @throws Exception
	 */
	private void addIsland( Vec2i loc, int value, MoveList moveList ) throws Exception {
		
		Island island = new Island( loc, value );
		try {
			removeIsland( loc, moveList );
		}
		catch( Exception exception ) {}

		assert !usageMap[loc.x][loc.y];
		
		islands.put( loc, island );
		usageMap[loc.x][loc.y] = true;
		moveList.addMove( new MoveAddIsland(loc,value) );
	}


	/**
	 * Removes an island at the specified location and appends to the move
	 * list while doing so. Will remove any bridge connected to this island.
	 * 
	 * @param loc The location at which to remove an island
	 * @param moveList The list of moves to populate
	 * @throws Exception
	 */
	private void removeIsland( Vec2i loc, MoveList moveList ) throws Exception {
		
		Island island = islands.get(loc);
		if( island == null ) throw new Exception( "Removing unexisting island" );	
		removeAllBridges( island, loc, moveList );
		
		islands.remove(loc);
		usageMap[loc.x][loc.y] = false;
		moveList.addMove( new MoveRemoveIsland(loc, island.getValue()) );		
	}
	
	/**
	 * Adds a bridge between the specified locations and appends to the move
	 * list while doing so. Will remove any bridge sitting directly between the
	 * same islands.
	 * 
	 * @param from The starting location of the bridge
	 * @param to The ending location of the bridge
	 * @param value The value of the bridge to add
	 * @param moveList The list of moves to populate
	 * @throws Exception
	 */
	private void addBridge( Vec2i from, Vec2i to, int value, MoveList moveList ) throws Exception {

		Island islandFrom = islands.get( from );
		Island islandTo = islands.get( to );
		if( islandFrom == null || islandTo == null )
			throw new Exception( "Adding a bridge where at least one end is not on an island" );
		int direction = Vec2i.findDirection( from, to );
		int oppositeDirection = Vec2i.oppositeDirection(direction);

		try {
			removeBridge( from, to, moveList );
		}
		catch( Exception exception ) {}

		checkLocationsValidForABridge( from, to );
		
		Bridge bridge = new Bridge( value );
		islandFrom.connectToBridge( direction, bridge );
		islandTo.connectToBridge( oppositeDirection, bridge );
		
		bridges.add( bridge );
		drawLineInUsageMap( from, to, true );
		moveList.addMove( new MoveAddBridge(from, to, value) );
	}
	
	/**
	 * Removes a bridge between the specified locations and appends to the move
	 * list while doing so.
	 * 
	 * @param from The starting location of the bridge
	 * @param to The ending location of the bridge
	 * @param moveList The list of moves to populate
	 * @throws Exception
	 */
	private void removeBridge( Vec2i from, Vec2i to, MoveList moveList ) throws Exception {

		Island islandFrom = islands.get( from );
		Island islandTo = islands.get( to );
		if( islandFrom == null || islandTo == null )
			throw new Exception( "Removing a bridge where at least one end is not on an island" );
		int direction = Vec2i.findDirection( from, to );		
		
		Bridge bridge = islandFrom.getBridge(direction);
		if( bridge == null ) throw new Exception( "Removing null bridge" );
		if( !bridge.connectedTo( islandTo ) ) throw new Exception( "Invalid coordinates for bridge removal" );
		
		removeBridgeNoCheck( bridge, from, to, moveList );
	}

	/**
	 * Remove a bridge but does not perform any sanity check
	 * 
	 * @param bridge Bridge to remove
	 * @param from One end of the bridge
	 * @param to The other end of the bridge
	 * @param moveList The list of moves to populate
	 * @throws Exception
	 */
	private void removeBridgeNoCheck(Bridge bridge, Vec2i from, Vec2i to, MoveList moveList) throws Exception {
		bridge.disconnect();
		bridges.remove( bridge );
		drawLineInUsageMap( from, to, false );
		moveList.addMove( new MoveRemoveBridge(from, to, bridge.getValue()) );		
	}

	/**
	 * Removes all the bridges connected to an island and populates the move
	 * list while doing so.
	 * 
	 * @param island The island from which to remove all bridges
	 * @param loc The location of the island
	 * @param move The list of moves to populate
	 * @throws Exception
	 */
	private void removeAllBridges(Island island, Vec2i loc, MoveList moveList) throws Exception {
		for( int i=0; i<4; ++i ) {
			Bridge bridge = island.getBridge(i);
			if( bridge != null ) {
				Island other = bridge.getOtherIsland( island );
				removeBridgeNoCheck( bridge, loc, other.getLoc(), moveList );
			}
		}
	}
	
	/**
	 * Checks if the path between two locations is valid for a bridge. Does not check
	 * to two extreme points since islands are expected to be sitting there.
	 * 
	 * @param from Starting location
	 * @param to Ending location
	 * @throws Exception
	 */
	private void checkLocationsValidForABridge(Vec2i from, Vec2i to) throws Exception {
		if( !isValidCell(from) || !isValidCell(to) )
			throw new Exception( "Invalid cell location" );
		
		int direction = Vec2i.findDirection( from, to );
		
		Vec2i current = new Vec2i(from);
		Vec2i.advanceInDirection( current, direction );
		while( current.x != to.x || current.y != to.y ) {
			if( usageMap[current.x][current.y] ) throw new Exception( "Invalid location for a bridge" );
			Vec2i.advanceInDirection( current, direction );
		}
	}
	

	/**
	 * Draw a horizontal or vertical line in the usage map. Do not draw 
	 * over the two extreme points.
	 * 
	 * @param from Starting location
	 * @param to Ending location
	 * @param value Value to draw in the map
	 * @throws Exception
	 */
	private void drawLineInUsageMap(Vec2i from, Vec2i to, boolean value ) throws Exception {
		if( !isValidCell(from) || !isValidCell(to) )
			throw new Exception( "Invalid cell location" );
		
		int direction = Vec2i.findDirection( from, to );
		
		Vec2i current = new Vec2i(from);
		Vec2i.advanceInDirection( current, direction );
		while( current.x != to.x || current.y != to.y ) {
			usageMap[current.x][current.y] = value;
			Vec2i.advanceInDirection( current, direction );
		}
	}	
	
	/**
	 * Resets the usage map entirely to false
	 */
	private void clearUsageMap() {
		usageMap = new Boolean[getSize().x][getSize().y];
		for (int i = 0; i < usageMap.length; i++) {
			for (int j = 0; j < usageMap[i].length; j++) {
				usageMap[i][j] = false;
			}
		}
	}

	/**
	 * Returns the value of the island at the specified location
	 * 
	 * @param loc Location of the island to check
	 * @return Value of the island at this location
	 * @throws Exception
	 */
	public int getIslandValue(Vec2i loc) throws Exception {
		Island island = islands.get(loc);
		if( island == null ) throw new Exception( "No island at location" );
		return island.getValue();
	}

	@Override
	public Move clear( long time ) {
		return performClear();
	}

	@Override
	public Move expandBottom(int delta) throws Exception {
		return performClear();
	}

	@Override
	public Move expandLeft(int delta) throws Exception {
		return performClear();
	}

	@Override
	public Move expandRight(int delta) throws Exception {
		return performClear();
	}

	@Override
	public Move expandTop(int delta) throws Exception {
		return performClear();
	}

	public void resize(int sizeX, int sizeY) throws Exception {
		performClear();
	}

	@Override
	public MoveList getMoveList() {
		MoveList moveList = super.getMoveList();
		appendToMoveList( moveList );
		return moveList;
	}

	/**
	 * Generates the movelist for the entire board and append it to the passed moveList
	 * 
	 * @param moveList The move list to append to
	 */
	private void appendToMoveList(MoveList moveList) {

		for (Iterator<Island> iterator = islands.values().iterator(); iterator.hasNext();) {
			Island island = iterator.next();
			moveList.addMove( new MoveAddIsland(island.getLoc(), island.getValue()) );
		}
		
		for (Iterator<Bridge> iterator = bridges.iterator(); iterator.hasNext();) {
			Bridge bridge = iterator.next();
			try {
				moveList.addMove( new MoveAddBridge(bridge.getLoc(0),bridge.getLoc(1),bridge.getValue()) );
			}
			catch( Exception exception ) {
				exception.printStackTrace();
			}
		}
		
	}	
	
	/**
	 * Resets the state of the entire puzzle
	 */
	private Move performClear() {
		islands.clear();
		bridges.clear();
		clearUsageMap();
		setChanged();
		return new MoveClearPuzzle(MoveImpl.NO_TIME);
	}

	@Override
	public boolean isSolutionValid(ComponentHighlighter highlighter,
			HasText textArea) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean checkSolution() {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public Move setCurrentStateAsSolution() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public PuzzleFactory getFactory() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public BoundInfo getBoundInfo() {
		return null;
	}

	
}
